
// Higher-level API for dealing with people searches.

var PeopleSearch = {};

(function () {

    PeopleSearch.findPeople = function (queryString, callback) {

        // First we do a Google Search for the query string to find pages
        // about whatever was entered.
        
        var searchArgs = {
            rsz: "large"
        };
        
        GoogleSearch.doSearch(queryString, searchArgs, function (result) {
            if (! result) {
                callback(undefined, makeError(500, "Search returned an invalid result"));
                return;
            }
            if (result.responseStatus != 200) {
                callback(undefined, makeError(result.responseStatus, result.responseDetails));
            }
            
            var items = result.responseData.results;
            
            // Extract just the URLs, keeping the result order so that we can
            // order the people results by it. We also track URLs that we've
            // not yet attached to people -- which at this point is all of them --
            // so that they can be returned a Page results later.
            var resultURLs = [];
            var justPageURLs = {};
            
            for (var i = 0; i < items.length; i++) {
                var url = items[i].url;
                resultURLs.push(url);
                justPageURLs[url] = items[i];
            }

            // Now we take the URLs we found and feed them into the Social Graph API
            // to find out:
            //  * Attributes such as names, usernames, profile URLs and so forth
            //  * Other URLs that represent the same person where we may find the attributes above.
            
            var sgapiArgs = {
                fme: true,
                sgn: true
            };
            
            SGAPI.fetchResults(resultURLs, sgapiArgs, function (result) {

                // Turn all of the node objects in the response into instances of our SocialNode class
                for (key in result.nodes) {
                    result.nodes[key] = new SocialNode(key, result.nodes[key]);
                }
                
                var people = [];

                for (var i = 0; i < resultURLs.length; i++) {
                    var url = resultURLs[i];
                    if (! justPageURLs[url]) continue; // We've already processed this URL.
                    var canonURL = result.canonical_mapping[url];
                    var primaryNode = result.nodes[canonURL];
                    
                    // Wha? Oh well.
                    if (! primaryNode) continue;
                    
                    if (! primaryNode.looksLikePerson()) continue;
                    delete justPageURLs[url];
                    
                    var nodes = getNodeMapForPrimaryNode(primaryNode, result.nodes);
                    
                    // Now we remove any other nodes we encountered from the "justPageURLs"
                    // map so we won't process them again if they came up later in the search
                    // results.
                    for (claimedNodeKey in nodes) {
                        var claimedNode = nodes[claimedNodeKey];
                        var claimedNodeURLs = claimedNode.getAllURLs();
                        for (var claimedNodeURLIndex = 0; claimedNodeURLIndex < claimedNodeURLs.length; claimedNodeURLIndex++) {
                            var claimedNodeURL = claimedNodeURLs[claimedNodeURLIndex];
                            delete justPageURLs[claimedNodeURL];
                        }
                    }

                    var person = new Person(primaryNode, nodes);
                    people.push(person);

                }

                // Anything left over in justPageURLs is a Page rather than a Person.
                var pages = [];
                
                for (var i = 0; i < items.length; i++) {
                    var url = items[i].url;
                    if (justPageURLs[url]) {
                        pages.push(new Page(items[i]));
                    }
                }

                callback(new PeopleSearchResult(people, pages), undefined);
                
            });
        
        });
    
    };

    /* PeopleSearchResult class */
    var PeopleSearchResult = function (people, pages) {
        this.people = people;
        this.pages = pages;
    };
    PeopleSearchResult.prototype = {};

    /* Person class */
    var Person = function (primaryNode, nodes) {
        this.primaryNodeKey = primaryNode.getKey();
        this.nodes = nodes;
    };
    Person.prototype = {};
    
    Person.prototype.getName = function () {
        var names = this.getNames();
        if (names && names[0]) {
            return names[0];
        }
        else {
            return null;
        }
    };
    Person.prototype.getNames = function () {
        var allNames = removeDuplicatesFromList(getAllResponses(this.nodes, "getName")).sort(compareFullNamesForQuality);
        return allNames;
    };
    Person.prototype.getUsernames = function () {
        var usernames = [];
        for (key in this.nodes) {
            if (key.match(/sgn:/)) {
                var result;
                if (result = key.match(/\?ident=(.*)$/)) {
                    usernames.push(result[1]);
                }
            }
        }
        return removeDuplicatesFromList(usernames).sort(compareUsernamesForQuality);
    };
    Person.prototype.getDisplayNames = function () {
        var names = this.getNames();
        var usernames = this.getUsernames();

        return removeDuplicatesFromList(concatenateLists(names, usernames));        
    };
    Person.prototype.getDisplayName = function () {
        var displayNames = this.getDisplayNames();
        if (displayNames && displayNames[0]) {
            return displayNames[0];
        }
        else {
            return null;
        }
    };
    Person.prototype.getPhotoURLs = function () {
        return getAllResponses(this.nodes, "getPhotoURL");
    };
    Person.prototype.getNodes = function () {
        var ret = [];
        for (key in this.nodes) {
            ret.push(this.nodes[key]);
        }
        return ret;
    };

    /* SocialPage class */
    var SocialNode = function (key, sgapiNode) {
        if (! sgapiNode) throw "sgapiNode is required";
        this.key = key;
        this.sgapiNode = sgapiNode;
    };
    SocialNode.prototype = {};
    SocialNode.prototype.getKey = function () {
        return this.key;
    };
    SocialNode.prototype.getClaimedNodeKeys = function () {
        return this.sgapiNode.claimed_nodes;
    };
    SocialNode.prototype.looksLikePerson = function () {
        if (this.sgapiNode.claimed_nodes.length > 0) return true;
        
        for (dummy in this.sgapiNode.attributes) {
            return true;
        }
    };
    SocialNode.prototype.getAllURLs = function () {
        var ret = [];
        var interestingAttributes = ["url", "profile", "atom", "rss", "blog"];
        for (var i = 0; i < interestingAttributes.length; i++) {
            if (this.sgapiNode.attributes[interestingAttributes[i]]) {
                ret.push(this.sgapiNode.attributes[interestingAttributes[i]]);
            }
        }
        return ret;
    };
    SocialNode.prototype.getName = function () {
        return this.sgapiNode.attributes.fn;
    };
    SocialNode.prototype.getPhotoURL = function () {
        return this.sgapiNode.attributes.photo;
    };
    SocialNode.prototype.getPrimaryURL = function () {
        return this.sgapiNode.attributes.url ? this.sgapiNode.attributes.url : this.key;
    };
    SocialNode.prototype.getSGNDomain = function () {
        var key = this.key;
        if (matches = key.match(/^sgn:\/\/([^\/]+)\//)) {
            return matches[1];
        }
        else {
            return null;
        }
    };
    SocialNode.prototype.getIconURL = function () {
        var key = this.key;
        var icon;
        var domain;
        if (domain = this.getSGNDomain()) {
            icon = domainFavicons[domain] ? domainFavicons[domain] : undefined;
            if (icon === true) icon = 'http://'+domain+'/favicon.ico';
        }
        else if (key.match(/^xmpp:/)) {
            icon = "http://xmpp.org/favicon.ico";
        }
        else if (key.match(/^callto:/)) {
            icon = "http://skype.com/favicon.ico";
        }
        return icon ? icon : 'other.png';
    };
    SocialNode.prototype.getUsername = function () {
        if (this.key.match(/sgn:/)) {
            var result;
            if (result = this.key.match(/\?ident=(.*)$/)) {
                return result[1];
            }
            else if (result = this.key.match(/\?pk=(.*)$/)) {
                return result[1];
            }
        }
        return this.key;
    };
    SocialNode.prototype.hasAssociatedIdentNode = function () {
        return this.sgapiNode.attributes.ident_is ? true : false;
    };

    /* Page class */
    var Page = function (searchResult) {
        this.searchResult = searchResult;
    };
    Page.prototype = {};

    /* Helper Methods */
    
    function makeError(code, message) {
        var ret = new Error();
        ret.name = code;
        ret.message = message;
        return ret;
    };

    function getNodeMapForPrimaryNode(primaryNode, resultNodes) {
        var nodes = {};
        populateNodeMapForPrimaryNode(primaryNode, resultNodes, nodes);
        return nodes;
    }

    function populateNodeMapForPrimaryNode(primaryNode, resultNodes, nodes) {
        var sgapiNode = primaryNode.sgapiNode;
        var primaryNodeKey = primaryNode.getKey();
        
        // If we've already visited this node, return immediately.
        if (nodes[primaryNodeKey]) return;
        
        nodes[primaryNodeKey] = primaryNode;
        
        var claimedNodeKeys = primaryNode.getClaimedNodeKeys();
        
        for (var i = 0; i < claimedNodeKeys.length; i++) {
            var claimedNodeKey = claimedNodeKeys[i];
            var claimedNode = resultNodes[claimedNodeKey];
            if (claimedNode) {
                populateNodeMapForPrimaryNode(claimedNode, resultNodes, nodes);
            }
        }
    }

    function compareFullNamesForQuality(a, b) {
        var qualityA = fullNameQualityScore(a);
        var qualityB = fullNameQualityScore(b);
        
        if (qualityA > qualityB) return -1;
        if (qualityB > qualityA) return 1;
        
        // Shorter forms prefered, so (for example) we get Alex Smith before Alexander Smith.
        if (a.length < b.length) return -1;
        if (b.length < a.length) return 1;
        
        return 0;
    }

    function fullNameQualityScore(name) {
        var score = 0;
        // If it contains at least one space
        if (name.match(/ /)) {
            score += 2;
        }
        
        // If it contains capital letters
        if (name.match(/[A-Z]/)) {
            score++;
        }
        
        // Don't want abbreviations marked with a dot
        if (name.match(/\./)) {
            score --;
        }

        return score;
    }

    function compareUsernamesForQuality(a, b) {
        var qualityA = usernameQualityScore(a);
        var qualityB = usernameQualityScore(b);
        
        if (qualityA > qualityB) return -1;
        if (qualityB > qualityA) return 1;
        
        // Shorter forms prefered, so (for example) we get Alex Smith before Alexander Smith.
        if (a.length < b.length) return -1;
        if (b.length < a.length) return 1;
        
        return 0;
    }

    function usernameQualityScore(name) {
        var score = 0;

        // If it contains capital letters
        if (name.match(/[A-Z]/)) {
            score += 2;
        }
        
        // If it contains letters at all
        if (name.match(/[a-zA-Z]/)) {
            score++;
        }
        
        // Prefer usernames without underscores
        if (name.match(/_/)) {
            score--;
        }

        // If it's all numeric, push it right to the bottom
        if (name.match(/^\d+$/)) {
            score -= 5;
        }

        // If it contains a slash, push it right to the bottom
        // (Facebook idents are odd-looking.)
        if (name.match(/\//)) {
            score -= 4;
        }

        return score;
    }

    function getAllResponses(list, method) {
        var ret = [];
        for (key in list) {
            var item = list[key];
            if (item) {
                var response = item[method]();
                if (response) ret.push(response);
            }
        }
        return ret;
    }

    function removeDuplicatesFromList(list) {
        var seen = {};
        var ret = [];
        for (var i = 0; i < list.length; i++) {
            var item = list[i];
            if (! seen[item]) ret.push(item);
            seen[item] = true;
        }
        return ret;
    }

    function concatenateLists(list1, list2) {
        var ret = [];
        for (var i = 0; i < list1.length; i++) {
            ret.push(list1[i]);
        }
        for (var i = 0; i < list2.length; i++) {
            ret.push(list2[i]);
        }
        return ret;    
    }

    var domainFavicons = {
        'livejournal.com': true,
        'last.fm': true,
        'myspace.com': true,
        'digg.com': true,
        'facebook.com': true,
        'linkedin.com': 'http://www.linkedin.com/favicon.ico',
        'jaiku.com': true,
        'meetup.com': true,
        'tribe.net': true,
        'youtube.com': true,
        'orkut.com': true,
        'aol.com': true,
        'reddit.com': true,
        'twitter.com': true,
        'del.icio.us': true,
        'ma.gnolia.com': true,
        'zooomr.com': true,
        'picasaweb.google.com': true,
        'amazon.com': true,
        'dopplr.com': true,
        'upcoming.yahoo.com': true,
        'bloglines.com': true,
        'yelp.com': true,
        'disqus.com': 'http://media.disqus.com/images/dsq-favicon-16x16.ico',
        'flickr.com': true,
        'friendfeed.com': true,
        'pownce.com': 'http://pownce.com/img/favicon.ico',
        'slideshare.net': 'http://www.slideshare.net/favicon.ico',
        'vox.com': 'http://static.vox.com/.shared/images/favicon.ico',
        'dodgeball.com': 'http://www.dodgeball.com/static/4021100690-favicon.ico',
        'blogger.com': 'http://www.blogger.com/favicon.ico',
        'reader.google.com': 'http://www.google.com/favicon.ico',
        'profiles.google.com': 'http://www.google.com/favicon.ico',
        'stumbleupon.com': 'http://www.stumbleupon.com/favicon.ico',
        'tumblr.com': 'http://www.tumblr.com/images/favicon.gif',
        'wordpress.com': true
    };

})();


